在前面的文章(第三章)中,我們已經詳細介紹了 HTTP 協議,包括它的結構、請求方法、狀態碼等內容。如果你對 HTTP 協議還不夠熟悉,可以回顧之前的篇章。今天,我們將進一步深入探討 REST API 設計,重點關注如何運用 HTTP 協議來構建高效且擴展性強的 API,並介紹一些進階設計原則與實踐。
REST API(Representational State Transfer Application Programming Interface)是一種基於 HTTP 協議的 API 設計風格。它通過資源導向的設計,使應用之間的數據交換和操作更加簡單和直觀。REST API 的核心是將「資源」作為中心,並充分利用 HTTP 協議的特性(如方法、狀態碼和 Headers)。
簡單來說,REST API 就像是一個針對資源的服務,每個資源都有自己的 URL,而對這些資源的操作則是通過不同的 HTTP 方法來完成的。
設計一個優秀的 REST API 需要遵循一些核心原則,這不僅能保證功能的實現,也有助於保持 API 的一致性、可擴展性和可維護性。
REST API 的設計應以「資源」為中心,而非行為或操作。每個資源都應該有一個唯一的 URI(統一資源標識符),資源通常是應用中的某個實體(如「用戶」或「訂單」)。設計時,應通過 URI 來訪問具體資源。
範例:
/users
:表示所有用戶的資源集合。/users/123
:表示 ID 為 123 的具體用戶資源。這樣的設計使得 API 使用者可以輕易理解 API 的結構,並直觀地知道如何訪問或操作資源。
HTTP 方法定義了對資源應該進行的操作。在 REST API 中,正確使用這些方法至關重要,因為它們賦予 API 自然且一致的行為。
正確的操作方法可以提升 API 的一致性與可讀性,讓開發者清楚知道每個請求應該如何工作。
REST API 是無狀態的,每一次的請求都是獨立的。伺服器不會保留客戶端的上下文信息,因此每個請求必須包含完成操作所需的所有信息。這種設計讓 API 更容易擴展,因為請求可以分散到不同的伺服器進行處理。
範例:
GET /users/123
Host: api.example.com
Authorization: Bearer <token>
這樣的請求包含了所有必需的信息,伺服器無需依賴之前的請求上下文。
REST API 通過 HTTP 狀態碼反饋請求結果。正確地使用狀態碼能夠幫助 API 使用者快速了解請求的結果或錯誤原因。
常見狀態碼:
當 API 返回大量數據時,應該支持過濾、分頁和排序,這不僅提高了效率,也使得客戶端能夠靈活地篩選和處理數據。
範例:
GET /users?age=25&limit=10&page=2&sort=created_at
這樣的請求會過濾出年齡為 25 的用戶,並按創建時間排序,返回第二頁的 10 個結果。
REST API 設計應保持簡潔和易於使用。過度設計會讓 API 變得難以理解和維護。應避免在 URI 中混入動詞或具體操作,如 /getAllUsers
或 /deleteUserById
。這些操作應通過 HTTP 方法來實現,而非通過 URI。
隨著 API 的演變,應用需求的變化可能會影響 API 的設計。因此,版本控制和擴展性在設計過程中是不可忽視的要素。
API 需要隨著時間推進進行升級或變更,因此保持對現有使用者的兼容性至關重要。常見的版本控制方法包括:
/v1/users
。Accept: application/vnd.example.v1+json
。/users?version=1
。在實際應用中,URI 版本控制最為常見和直觀。
在設計 API 時,要盡量避免對現有字段的移除或修改。應通過新增字段或路由來進行擴展,以保證對現有使用者的兼容性。
範例:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"new_field": "new_value" // 新增字段,保持向後兼容
}
這樣設計的 API 可以保持現有功能不變,並允許新需求的擴展。
以下是一個設計不佳的 REST API 範例,並解釋了其中的問題:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private static List<User> Users = new List<User>
{
new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 },
new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 25 }
};
// GET: api/users
[HttpGet("action")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult<IEnumerable<User>> GetAllUsers()
{
return Ok(Users);
}
// POST: api/users/action/add
[HttpPost("action/add")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult<User> CreateNewUser([FromBody] User newUser)
{
newUser.Id = Users.Max(u => u.Id) + 1;
Users.Add(newUser);
return Ok(newUser); // 設計錯誤:應該返回 201 Created
}
// PUT: api/users/action/update/{id}
[HttpPut("action/update/{id}")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult UpdateUserDetails(int id, [FromBody] User updatedUser)
{
var user = Users.FirstOrDefault(u => u.Id == id);
if (user == null)
{
return NotFound();
}
user.Name = updatedUser.Name;
user.Email = updatedUser.Email;
user.Age = updatedUser.Age;
return Ok(user); // 設計錯誤:應該返回 204 No Content
}
// DELETE: api/users/action/delete/{id}
[HttpDelete("action/delete/{id}")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult RemoveUser(int id)
{
var user = Users.FirstOrDefault(u => u.Id == id);
if (user == null)
{
return NotFound();
}
Users.Remove(user);
return Ok(); // 設計錯誤:應該返回 204 No Content
}
// 自定義操作方法,存在設計缺陷
[HttpPost("custom-action")] // 設計錯誤:不明確的操作,無法清楚理解該操作的目的
public ActionResult CustomOperation([FromBody] CustomRequest request)
{
// 根據 OperationType 字段來決定具體的操作
if (request.OperationType == "updateEmail")
{
var user = Users.FirstOrDefault(u => u.Id == request.UserId);
if (user == null)
{
return NotFound("User not found");
}
user.Email = request.Data; // 更新用戶的電子郵件
}
else if (request.OperationType == "deleteUser")
{
var user = Users.FirstOrDefault(u => u.Id == request.UserId);
if (user == null)
{
return NotFound("User not found");
}
Users.Remove(user); // 刪除用戶
}
else if (request.OperationType == "addNote")
{
var user = Users.FirstOrDefault(u => u.Id == request.UserId);
if (user == null)
{
return NotFound("User not found");
}
// 為用戶添加備註(在現有的資料上增加新數據)
// 假設用戶模型中存在 Notes 屬性
user.Notes.Add(request.Data);
}
else
{
return BadRequest("Unknown operation");
}
// 無論進行什麼操作,最終都返回相同的回應
return Ok("Custom operation performed");
}
}
public class CustomRequest
{
public string OperationType { get; set; }
public int UserId { get; set; }
public string Data { get; set; }
}
action
和其他具體操作動詞,如 action/add
,這違背了資源導向的設計原則。應將 URI 專注於資源,而非操作行為。/users
來表示所有用戶,使用 /users/{id}
來表示具體用戶,而不是 action/add
等不清晰的操作。CreateNewUser
和 UpdateUserDetails
中,錯誤地使用了 200 OK
作為回應。正確的設計應該是使用 201 Created
和 204 No Content
。201 Created
,並返回新資源的 URI;對於更新操作,應使用 204 No Content
。200 OK
,而不是更合適的 204 No Content
,這讓使用者無法準確理解操作結果。CustomOperation
使用 OperationType
來決定執行的邏輯,這違反了單一職責原則,增加了 API 的複雜度,讓使用者難以理解具體的操作內容。custom-action
允許一個請求執行多種不同的操作,例如更新用戶的電子郵件、刪除用戶、或添加備註。OperationType
字段決定的。這與 REST API 的設計原則背道而馳,因為 REST API 強調「資源導向」,每個操作應該有一個明確且一致的 URI 和 HTTP 方法。updateEmail
應該有一個獨立的端點,如 PUT /users/{id}/email
,而 deleteUser
應該對應 DELETE /users/{id}
,這樣使用者可以直觀地知道每個端點具體執行什麼操作。"Custom operation performed"
。這並沒有提供足夠的上下文來告訴客戶端實際完成了什麼操作。如果發生錯誤或某些操作部分完成,用戶將無法得知具體情況。200 OK
。然而,正確的做法是針對不同操作使用相應的狀態碼。例如:
204 No Content
。201 Created
。204 No Content
。[ApiController]
[Route("api/[controller]")] // 定義這個控制器的路由,"api/[controller]" 會自動將控制器名稱替換為 "users"
public class UsersController : ControllerBase
{
// 模擬用戶資料的靜態列表
private static List<User> Users = new List<User>
{
new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 }, // 預設用戶 1
new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 25 } // 預設用戶 2
};
// GET: api/users
[HttpGet] // 定義 GET 請求,用於獲取所有用戶資料
public ActionResult<IEnumerable<User>> GetUsers()
{
// 返回 200 OK 狀態碼,並傳回用戶列表
return Ok(Users);
}
// POST: api/users
[HttpPost] // 定義 POST 請求,用於創建新的用戶
public ActionResult<User> CreateUser([FromBody] User newUser)
{
// 分配新用戶的 ID,使用現有用戶的最大 ID 加 1
newUser.Id = Users.Max(u => u.Id) + 1;
// 將新用戶加入用戶列表
Users.Add(newUser);
// 返回 201 Created 狀態碼,並附上新創建用戶
return CreatedAtAction(nameof(GetUsers), new { id = newUser.Id }, newUser);
}
// PUT: api/users/{id}
[HttpPut("{id}")] // 定義 PUT 請求,用於更新指定 ID 的用戶資料
public ActionResult UpdateUser(int id, [FromBody] User updatedUser)
{
// 根據 ID 查找現有用戶
var user = Users.FirstOrDefault(u => u.Id == id);
// 如果用戶不存在,返回 404 Not Found
if (user == null)
{
return NotFound();
}
// 更新用戶的資料
user.Name = updatedUser.Name;
user.Email = updatedUser.Email;
user.Age = updatedUser.Age;
// 返回 204 No Content,表示更新成功但不需要回傳內容
return NoContent();
}
// DELETE: api/users/{id}
[HttpDelete("{id}")] // 定義 DELETE 請求,用於刪除指定 ID 的用戶
public ActionResult DeleteUser(int id)
{
// 根據 ID 查找現有用戶
var user = Users.FirstOrDefault(u => u.Id == id);
// 如果用戶不存在,返回 404 Not Found
if (user == null)
{
return NotFound();
}
// 從列表中移除用戶
Users.Remove(user);
// 返回 204 No Content,表示刪除成功但不需要回傳內容
return NoContent();
}
}
設計一個優秀的 REST API 不僅需要實現功能,還必須著眼於 API 的可用性、可維護性和擴展性。堅持資源導向設計、正確使用 HTTP 方法和狀態碼、保持無狀態性、並考慮版本控制和擴展性,這些設計原則能讓 API 更加一致、易於使用且具備彈性。通過避免常見的設計錯誤,我們可以提升 API 的品質,讓使用者在開發和使用過程中獲得更好的體驗。